Skip to content

Try every configured comment marker when scanning for callouts - #362

Open
youdie006 wants to merge 1 commit into
gomarkdown:masterfrom
youdie006:callout-try-every-comment
Open

Try every configured comment marker when scanning for callouts#362
youdie006 wants to merge 1 commit into
gomarkdown:masterfrom
youdie006:callout-try-every-comment

Conversation

@youdie006

Copy link
Copy Markdown

RendererOptions.Comments is documented at html/renderer.go:112 as "a list of comments the renderer should detect", and it is a [][]byte. EscapeHTMLCallouts loops over that list, but breaks out of the loop on the first non-match (html/renderer.go:888):

for _, comment := range r.Opts.Comments {
    if !bytes.HasPrefix(d[i:], comment) {
        break
    }

The loop is a search over candidate markers, so a non-match should move to the next candidate. Because it breaks instead, only Comments[0] is ever live and every later entry is dead.

The symptom is that reordering the same set changes the output:

comments = [// #]  ->  <pre><code>print(1) #&lt;&lt;1&gt;&gt;\n</code></pre>
comments = [# //]  ->  <pre><code>print(1) <span class="callout">1</span>\n</code></pre>

Same two markers, same source, different HTML.

Your other prefix-list scanners try every entry

  • parser/url.go:21 -- IsSafeURL loops Paths and continues past a non-candidate
  • parser/url.go:38 -- the same over URIs
  • parser/inline.go:487 -- maybeAutoLink loops protocolPrefixes, also [][]byte, also a fixed-offset HasPrefix test, and falls through to the next prefix

Three sites with the same shape, and the callout loop is the only one that stops at the first miss.

The Parse: label points the same way: the success path does continue Parse, which only earns its keep if the inner loop can reach candidate 2. Today it cannot.

Why it went unnoticed

html/callouts_test.go:19 sets opts.Comments = [][]byte{[]byte("//")} -- a one-element list, where break and continue are indistinguishable. So the existing test could not have caught this.

The fix

break -> continue, one line. A test covering both orderings of [// #], with a third line using no marker so the escape path stays pinned.

Behaviour change

Only for callers configuring two or more markers. With a single marker the two keywords are identical, so nothing changes there. No existing test row has to change -- the suite is green unmodified.

Verification

Exactly what the workflow runs (go test -v . && ./ast && ./parser && ./html):

ok  github.com/gomarkdown/markdown        0.067s
ok  github.com/gomarkdown/markdown/ast    0.004s
ok  github.com/gomarkdown/markdown/parser 0.014s
ok  github.com/gomarkdown/markdown/html   0.005s

go vet ./... clean. gofmt -l does not list either file I touched (it does list five pre-existing files elsewhere in the tree, which I left alone).

I checked the new test pins the iteration rather than the keyword, from three directions:

mutation result
continue back to break fails
remove the marker guard entirely, so every <<N>> becomes a callout fails, caught by the unmarked c <<9>> row
keep continue but iterate Comments[:1] fails

The third one is the point: it fails for the same reason the original does, which shows the test is about reaching the later entries and not about the // vs # keywords.

What I did not change

EscapeHTML and the Escaper table, the i+lc < ld bound (correct as-is -- IsCallout needs at least five bytes), parser.IsCallout, and the Comments != nil dispatch in CodeBlock at html/renderer.go:925.


Disclosure: AI-assisted. I found and prepared this with an AI assistant, and I ran and verified everything above myself.

The inner loop in EscapeHTMLCallouts searches the configured markers,
but breaks out of that search on the first non-match, so only
Comments[0] can ever match and later entries are dead.

Reordering the same set changes the output: with [// #] a line marked
with # renders escaped, and with [# //] a line marked with // does.

IsSafeURL and maybeAutoLink both scan their prefix lists to the end;
this is the only one that stops at the first miss. The Parse: label and
its continue Parse on the success path show the inner loop was meant to
be a search.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant